package com.malty.gateway.sign;

import com.malty.common.ErrorCode;
import com.malty.common.exception.BusinessException;
import javafx.util.Pair;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * 签名策略接口
 * <p>
 * 负责根据请求提取签名参数，构造待签名字符串，并计算签名。
 * </p>
 * <p>
 * 实现类应根据具体签名算法重写 {@link #doCalculateSign(String, String)} 和 {@link #getParamMap(ServerWebExchange)}。
 * </p>
 *
 * @author malty
 */
@Slf4j
public abstract class ApiSignStrategy {
    /**
     * 计算签名
     *
     * @param signKey  签名密钥
     * @param exchange 当前HTTP请求
     * @return
     */
    public Mono<Void> checkSign(String signKey, ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        return calculateSign(signKey, exchange).flatMap(newSign -> {
            String originSign = request.getHeaders().getFirst("sign");
            if (originSign == null || !originSign.equals(newSign.getKey())) {
                log.error("签名错误，签名key:{}, 待签名原文:{}, 请求的签名:{} , 计算的签名:{}",
                        signKey, newSign.getValue(), originSign, newSign.getKey());
                return Mono.error(new BusinessException(ErrorCode.PARAMS_ERROR, "签名错误"));
            }
            return Mono.empty();
        });
    }

    protected Mono<Pair<String, String>> calculateSign(String signKey, ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        validateSignParam(request);
        return getParamMap(exchange).flatMap(paramMap -> {
            String paramString = buildParamString(paramMap);
            String newSign = doCalculateSign(signKey, paramString);
            return Mono.just(new Pair<>(newSign, paramString));
        });
    }

    /**
     * 计算签名，根据签名密钥和待签名字符串生成签名值
     *
     * @param signKey   签名密钥
     * @param sourceStr 待签名字符串
     * @return 签名结果，不得为null
     */
    protected abstract String doCalculateSign(String signKey, String sourceStr);

    /**
     * 从请求中提取所有参与签名的参数。
     * 实现类需复写此方法，根据请求方法、Content-Type等提取参数（header、query、body等）。
     *
     * @param exchange 当前HTTP请求
     * @return 包含签名参与参数的不可变Map，不得为null
     */
    protected abstract Mono<Map<String, String>> getParamMap(ServerWebExchange exchange);


    /**
     * 根据参数Map构建待签名字符串，默认实现：
     * <ul>
     *   <li>参数按key升序排序</li>
     *   <li>忽略空key或空value</li>
     *   <li>拼接格式为 k1=v1&k2=v2</li>
     * </ul>
     *
     * @param params 待签名参数Map
     * @return 拼接成字符串，不为null，空参数返回空字符串
     */
    protected String buildParamString(Map<String, String> params) {
        if (params == null || params.isEmpty()) {
            return "";
        }
        Map<String, String> sortedParams = new TreeMap<>(params);
        return sortedParams.entrySet().stream()
                .filter(e -> e.getKey() != null && !e.getKey().isEmpty())
                .filter(e -> e.getValue() != null && !e.getValue().isEmpty())
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.joining("&"));
    }

    /**
     * 校验请求中签名必需参数是否合法。
     * 默认校验 {@code timestamp} 和 {@code nonce} 是否存在且非空。
     * 实现类可覆盖该方法进行自定义校验。
     *
     * @param request 当前请求
     * @throws IllegalArgumentException 如果缺少必需参数
     */
    protected void validateSignParam(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();

        String timestamp = headers.getFirst("timestamp");
        if (timestamp == null || timestamp.isEmpty()) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "Missing required header: timestamp");
        }

        String nonce = headers.getFirst("nonce");
        if (nonce == null || nonce.isEmpty()) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "Missing required header: nonce");
        }
    }

    /**
     * 从请求头中提取签名相关参数，默认提取 {@code timestamp} 和 {@code nonce}。
     * <p>如果没有对应的header，返回的Map中对应key的value可能为null，建议调用 {@link #validateSignParam(ServerHttpRequest)} 进行校验。</p>
     *
     * @param request 当前HTTP请求
     * @return 包含header签名参数的不可变Map，不得为null
     */
    protected Map<String, String> extractHeaderParam(ServerHttpRequest request) {
        if (request == null) {
            return Collections.emptyMap();
        }
        HttpHeaders headers = request.getHeaders();
        Map<String, String> headerMap = new HashMap<>(2);

        // 允许value为空，交由validateSignParam校验
        headerMap.put("timestamp", headers.getFirst("timestamp"));
        headerMap.put("nonce", headers.getFirst("nonce"));

        return Collections.unmodifiableMap(headerMap);
    }
}
